//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//

package com.strangeberry.jmdns;

import java.io.IOException;
import java.net.DatagramPacket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * Parse an incoming DNS message into its components.
 * 
 * @author Arthur van Hoff, Werner Randelshofer, Pierre Frisch
 * @version %I%, %G%
 */
final class DNSIncoming {
    private static Logger logger = Logger.getLogger(DNSIncoming.class.toString());

    // Implementation note: This vector should be immutable.
    // If a client of DNSIncoming changes the contents of this vector,
    // we get undesired results. To fix this, we have to migrate to
    // the Collections API of Java 1.2. i.e we replace Vector by List.
    // final static Vector EMPTY = new Vector();

    private DatagramPacket packet;

    private int off;

    private int len;

    private byte data[];

    int id;

    private int flags;

    private int numQuestions;

    int numAnswers;

    private int numAuthorities;

    private int numAdditionals;

    private long receivedTime;

    List<DNSQuestion> questions;

    List<DNSRecord> answers;

    /**
     * Parse a message from a datagram packet.
     */
    DNSIncoming(DatagramPacket packet) throws IOException {
        this.packet = packet;
        this.data = packet.getData();
        this.len = packet.getLength();
        this.off = packet.getOffset();
        this.questions = Collections.emptyList();
        this.answers = Collections.emptyList();
        this.receivedTime = System.currentTimeMillis();

        try {
            id = readUnsignedShort();
            flags = readUnsignedShort();
            numQuestions = readUnsignedShort();
            numAnswers = readUnsignedShort();
            numAuthorities = readUnsignedShort();
            numAdditionals = readUnsignedShort();

            // parse questions
            if (numQuestions > 0) {
                questions = Collections.synchronizedList(new ArrayList<DNSQuestion>(numQuestions));
                for (int i = 0; i < numQuestions; i++) {
                    DNSQuestion question = new DNSQuestion(readName(), readUnsignedShort(), readUnsignedShort());
                    questions.add(question);
                }
            }

            // parse answers
            int n = numAnswers + numAuthorities + numAdditionals;
            if (n > 0) {
                answers = Collections.synchronizedList(new ArrayList<DNSRecord>(n));
                for (int i = 0; i < n; i++) {
                    String domain = readName();
                    int type = readUnsignedShort();
                    int clazz = readUnsignedShort();
                    int ttl = readInt();
                    int len = readUnsignedShort();
                    int end = off + len;
                    DNSRecord rec = null;

                    switch (type) {
                    case DNSConstants.TYPE_A: // IPv4
                    case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14 2004] This has not been tested
                        rec = new DNSRecord.Address(domain, type, clazz, ttl, readBytes(off, len));
                        break;
                    case DNSConstants.TYPE_CNAME:
                    case DNSConstants.TYPE_PTR:
                        rec = new DNSRecord.Pointer(domain, type, clazz, ttl, readName());
                        break;
                    case DNSConstants.TYPE_TXT:
                        rec = new DNSRecord.Text(domain, type, clazz, ttl, readBytes(off, len));
                        break;
                    case DNSConstants.TYPE_SRV:
                        rec = new DNSRecord.Service(domain, type, clazz, ttl, readUnsignedShort(), readUnsignedShort(), readUnsignedShort(), readName());
                        break;
                    case DNSConstants.TYPE_HINFO:
                        // Maybe we should do something with those
                        break;
                    default:
                        logger.finer("DNSIncoming() unknown type:" + type);
                        break;
                    }

                    if (rec != null) {
                        // Add a record, if we were able to create one.
                        answers.add(rec);
                    } else {
                        // Addjust the numbers for the skipped record
                        if (answers.size() < numAnswers) {
                            numAnswers--;
                        } else if (answers.size() < numAnswers + numAuthorities) {
                            numAuthorities--;
                        } else if (answers.size() < numAnswers + numAuthorities + numAdditionals) {
                            numAdditionals--;
                        }
                    }
                    off = end;
                }
            }
        } catch (IOException e) {
            logger.log(Level.WARNING, "DNSIncoming() dump " + print(true) + "\n exception ", e);
            throw e;
        }
    }

    /**
     * Check if the message is a query.
     */
    boolean isQuery() {
        return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY;
    }

    /**
     * Check if the message is truncated.
     */
    boolean isTruncated() {
        return (flags & DNSConstants.FLAGS_TC) != 0;
    }

    /**
     * Check if the message is a response.
     */
    boolean isResponse() {
        return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_RESPONSE;
    }

    private int get(int off) throws IOException {
        if ((off < 0) || (off >= len)) {
            throw new IOException("parser error: offset=" + off);
        }
        return data[off] & 0xFF;
    }

    private int readUnsignedShort() throws IOException {
        return (get(off++) << 8) + get(off++);
    }

    private int readInt() throws IOException {
        return (readUnsignedShort() << 16) + readUnsignedShort();
    }

    private byte[] readBytes(int off, int len) throws IOException {
        byte bytes[] = new byte[len];
        System.arraycopy(data, off, bytes, 0, len);
        return bytes;
    }

    private void readUTF(StringBuilder buf, int off, int len) throws IOException {
        for (int end = off + len; off < end;) {
            int ch = get(off++);
            switch (ch >> 4) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
                // 0xxxxxxx
                break;
            case 12:
            case 13:
                // 110x xxxx 10xx xxxx
                ch = ((ch & 0x1F) << 6) | (get(off++) & 0x3F);
                break;
            case 14:
                // 1110 xxxx 10xx xxxx 10xx xxxx
                ch = ((ch & 0x0f) << 12) | ((get(off++) & 0x3F) << 6) | (get(off++) & 0x3F);
                break;
            default:
                // 10xx xxxx, 1111 xxxx
                ch = ((ch & 0x3F) << 4) | (get(off++) & 0x0f);
                break;
            }
            buf.append((char) ch);
        }
    }

    private String readName() throws IOException {
        StringBuilder buf = new StringBuilder();
        int off = this.off;
        int next = -1;
        int first = off;

        while (true) {
            int len = get(off++);
            if (len == 0) {
                break;
            }
            switch (len & 0xC0) {
            case 0x00:
                // buf.append("[" + off + "]");
                readUTF(buf, off, len);
                off += len;
                buf.append('.');
                break;
            case 0xC0:
                // buf.append("<" + (off - 1) + ">");
                if (next < 0) {
                    next = off + 1;
                }
                off = ((len & 0x3F) << 8) | get(off++);
                if (off >= first) {
                    throw new IOException("bad domain name: possible circular name detected");
                }
                first = off;
                break;
            default:
                throw new IOException("bad domain name: '" + buf + "' at " + off);
            }
        }
        this.off = (next >= 0) ? next : off;
        return buf.toString();
    }

    /**
     * Debugging.
     */
    String print(boolean dump) {
        StringBuilder buf = new StringBuilder();
        buf.append(toString() + "\n");
        for (Iterator<DNSQuestion> iterator = questions.iterator(); iterator.hasNext();) {
            buf.append("    ques:" + iterator.next() + "\n");
        }
        int count = 0;
        for (Iterator<DNSRecord> iterator = answers.iterator(); iterator.hasNext(); count++) {
            if (count < numAnswers) {
                buf.append("    answ:");
            } else if (count < numAnswers + numAuthorities) {
                buf.append("    auth:");
            } else {
                buf.append("    addi:");
            }
            buf.append(iterator.next() + "\n");
        }
        if (dump) {
            for (int off = 0, len = packet.getLength(); off < len; off += 32) {
                int n = Math.min(32, len - off);
                if (off < 10) {
                    buf.append(' ');
                }
                if (off < 100) {
                    buf.append(' ');
                }
                buf.append(off);
                buf.append(':');
                for (int i = 0; i < n; i++) {
                    if ((i % 8) == 0) {
                        buf.append(' ');
                    }
                    buf.append(Integer.toHexString((data[off + i] & 0xF0) >> 4));
                    buf.append(Integer.toHexString((data[off + i] & 0x0F) >> 0));
                }
                buf.append("\n");
                buf.append("    ");
                for (int i = 0; i < n; i++) {
                    if ((i % 8) == 0) {
                        buf.append(' ');
                    }
                    buf.append(' ');
                    int ch = data[off + i] & 0xFF;
                    buf.append(((ch > ' ') && (ch < 127)) ? (char) ch : '.');
                }
                buf.append("\n");

                // limit message size
                if (off + 32 >= 256) {
                    buf.append("....\n");
                    break;
                }
            }
        }
        return buf.toString();
    }

    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append(isQuery() ? "dns[query," : "dns[response,");
        if (packet.getAddress() != null)
            buf.append(packet.getAddress().getHostAddress());
        buf.append(':');
        buf.append(packet.getPort());
        buf.append(",len=");
        buf.append(packet.getLength());
        buf.append(",id=0x");
        buf.append(Integer.toHexString(id));
        if (flags != 0) {
            buf.append(",flags=0x");
            buf.append(Integer.toHexString(flags));
            if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0) {
                buf.append(":r");
            }
            if ((flags & DNSConstants.FLAGS_AA) != 0) {
                buf.append(":aa");
            }
            if ((flags & DNSConstants.FLAGS_TC) != 0) {
                buf.append(":tc");
            }
        }
        if (numQuestions > 0) {
            buf.append(",questions=");
            buf.append(numQuestions);
        }
        if (numAnswers > 0) {
            buf.append(",answers=");
            buf.append(numAnswers);
        }
        if (numAuthorities > 0) {
            buf.append(",authorities=");
            buf.append(numAuthorities);
        }
        if (numAdditionals > 0) {
            buf.append(",additionals=");
            buf.append(numAdditionals);
        }
        buf.append("]");
        return buf.toString();
    }

    /**
     * Appends answers to this Incoming.
     * 
     * @exception InvalidArgumentException if this is not truncated, and that or this is not a query.
     */
    void append(DNSIncoming that) {
        if (this.isQuery() && this.isTruncated() && that.isQuery()) {
            this.questions.addAll(that.questions);
            this.numQuestions += that.numQuestions;

            if (Collections.EMPTY_LIST.equals(answers))
                answers = Collections.synchronizedList(new ArrayList<DNSRecord>());

            if (that.numAnswers > 0) {
                this.answers.addAll(this.numAnswers, that.answers.subList(0, that.numAnswers));
                this.numAnswers += that.numAnswers;
            }
            if (that.numAuthorities > 0) {
                this.answers.addAll(this.numAnswers + this.numAuthorities, that.answers.subList(that.numAnswers, that.numAnswers + that.numAuthorities));
                this.numAuthorities += that.numAuthorities;
            }
            if (that.numAdditionals > 0) {
                this.answers.addAll(that.answers.subList(that.numAnswers + that.numAuthorities, that.numAnswers + that.numAuthorities + that.numAdditionals));
                this.numAdditionals += that.numAdditionals;
            }
        } else {
            throw new IllegalArgumentException();
        }
    }

    int elapseSinceArrival() {
        return (int) (System.currentTimeMillis() - receivedTime);
    }
}
